/*
 * Copyright 2008,2009,2010 Daniel Freitas
 * 
 * This file is part of DMF Generic DAO.
 * 
 * DMF Generic DAO is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * DMF Generic DAO is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with DMF Generic DAO. If not, see
 * <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dmf.genericdao.spring;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;

import com.googlecode.dmf.genericdao.GenericDao;
import com.googlecode.dmf.genericdao.annotaions.Dao;
import com.googlecode.dmf.genericdao.hibernate.HibernateDaoAdvisor;
import com.googlecode.dmf.genericdao.hibernate.HibernateGenericDao;
import com.googlecode.dmf.genericdao.hibernate.HibernateQueryExecutor;
import com.googlecode.dmf.genericdao.spring.scanners.IPackageScanner;
import com.googlecode.dmf.genericdao.spring.scanners.LocalFileSystemDirectoryScanner;
import com.googlecode.dmf.genericdao.spring.scanners.LocalFileSystemJarScanner;
import com.googlecode.dmf.genericdao.util.BaseReflectioner;

/**
 * A {@link BeanFactoryPostProcessor} that implements the {@link ProxyDaoScannerFactory} to add proxies for annotated
 * generic dao interfaces as a post process step of spring bean factory's initialization.
 * <p/>
 * Scanning of packages will start after bean factory has been initialized but before any beans have been instantiated.
 * <p/>
 * Scanning and instantiation of daos proxies are performed by the current Thread's context classloader.
 * 
 * @author <a href="mailto:daniel.mfreitas@gmail.com">Daniel Freitas</a>
 */
public class GenericDaoProxyConfigurer implements BeanFactoryPostProcessor, ProxyDaoScannerFactory {

    private static final String DEFAULT_HIBERNATE_GENERIC_DAO_ADVISOR_BEAN_NAME = "com.googlecode.dmf.genericdao.defaultHibernateGenericDaoAdvisor";

    private String basePackage;

    private final Map<String, IPackageScanner> scannerRegistry = new HashMap<String, IPackageScanner>();
    private SessionFactory sessionFactory;

    private HibernateGenericDao<?> defaultHibernateGenericDao;

    /**
     * Contructs a proxy configurer with default package scanners. The default scanners support both the file and jar
     * file system protocols (i.e. files and jars on the machine's local file system).
     */
    public GenericDaoProxyConfigurer() {
        registerDefaultScanners();
    }

    /**
     * Registers a package scanner on this proxy configurer. Only one scanner per protocol is supported. If multiple
     * scanners for the same protocol are added, only the last one will take effect.
     *<p/>
     * Note that a {@link LocalFileSystemDirectoryScanner} and a {@link LocalFileSystemJarScanner} are added by default
     * upon initialization. See {@link GenericDaoProxyConfigurer#GenericDaoProxyConfigurer()} for details.
     * 
     * @param scanner
     *            The scanner to be registered.
     */
    public void registerScanner(final IPackageScanner scanner) {
        scannerRegistry.put(scanner.supportsProtocol(), scanner);
    }

    /**
     * Checks if this dao configurer supports the requested protocol.
     * 
     * @param protocol
     *            The protocol to check.
     * @return <tt>true</tt> if there is a scanner registered for that protocol, <tt>false</tt> otherwise.
     */
    public boolean supportsProtocol(final String protocol) {
        return scannerRegistry.containsKey(protocol);
    }

    /**
     * Defines the package scanners to be registered. Calling this method will unregister any previously registered
     * scanners. Note that the one-scanner-per-protocol rule is still in effect: if multiple scanners supports the same
     * protocol, only the last one will be taken into consideration.
     * 
     * @param scanners
     *            The list of scanners to be registerd.
     */
    public void setScanners(final List<IPackageScanner> scanners) {

        scannerRegistry.clear();

        for (IPackageScanner scanner : scanners) {
            registerScanner(scanner);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor#postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory)
     */
    @Override
    public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException {

        if (basePackage == null) {
            throw new IllegalStateException("GenericDaoProxyConfigurer not properly configured. "
                    + "Base package to scan has not been provided.");
        }

        createProxiesForDaos(basePackage, (BeanDefinitionRegistry) beanFactory);
    }

    /**
     * 
     * {@inheritDoc}
     * 
     * @return An array of {@link Class} objects of the classes and interfaces under the base package.
     * 
     * @see com.googlecode.dmf.genericdao.spring.ProxyDaoScannerFactory#createProxiesForDaos(java.lang.String)
     */
    public void createProxiesForDaos(final String basePackage, final BeanDefinitionRegistry beanRegistry) {
        createDefaultHibernateGenericDao();
        registerDefaultGenericDaoInterceptor(beanRegistry);

        try {
            URL resource = Thread.currentThread().getContextClassLoader().getResource(
                    basePackage.replaceAll("\\.", "/"));

            if (resource == null) {
                throw new ConfigurerException(MessageFormat.format(
                        "A resource for package '{0}' could not be found in "
                                + "the classpath. Scanning for DAOs aborted.", basePackage));
            }

            List<Class<?>> scannedClasses = null;

            IPackageScanner scanner = scannerRegistry.get(resource.getProtocol());

            if (scanner == null) {
                throw new UnsupportedOperationException(MessageFormat.format(
                        "The protocol for the resource {0} is not supported.", resource.toString()));
            }

            scannedClasses = scanner.scan(resource.toURI(), basePackage);

            for (Class<?> clazz : scannedClasses) {
                if (clazz.isInterface() && clazz.isAnnotationPresent(Dao.class)) {
                    addProxyToBeanRegistry(clazz, beanRegistry);
                }
            }
        } catch (URISyntaxException e) {
            throw new ConfigurerException(MessageFormat.format(
                    "An error has been detected while scanning for dao interfaces. {0}", e.getMessage()), e);
        } catch (IOException e) {
            throw new ConfigurerException(MessageFormat.format(
                    "An I/O error has been detected while scanning for dao interfaces. {0}", e.getMessage()), e);
        }
    }

    private void addProxyToBeanRegistry(final Class<?> clazz, final BeanDefinitionRegistry beanRegistry) {

        MutablePropertyValues properties = new MutablePropertyValues();

        properties.add("target", defaultHibernateGenericDao);
        properties.add("interceptorNames", Arrays
                .asList(new String[] { DEFAULT_HIBERNATE_GENERIC_DAO_ADVISOR_BEAN_NAME }));
        properties.add("proxyInterfaces", clazz.getName());

        RootBeanDefinition beanDefinition = new RootBeanDefinition(ProxyFactoryBean.class, null, properties);

        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, StringUtils.uncapitalize(clazz
                .getSimpleName()));

        BeanDefinitionReaderUtils.registerBeanDefinition(holder, beanRegistry);
    }

    /**
     * Returns the base package this component will scan for annotated dao interfaces upon initialization of the spring
     * context.
     * 
     * @return The base package name.
     * 
     * @see Dao
     * @see GenericDaoProxyConfigurer#createProxiesForDaos(String)
     * @see BeanFactoryPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory)
     */
    public String getBasePackage() {
        return basePackage;
    }

    /**
     * Sets the base package this component will scan for annotated dao interfaces upon initialization of the spring
     * context.
     * 
     * @param basePackage
     *            The base package name.
     * 
     * @see Dao
     * @see GenericDaoProxyConfigurer#createProxiesForDaos(String)
     * @see BeanFactoryPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory)
     */
    public void setBasePackage(final String basePackage) {
        this.basePackage = basePackage;
    }

    /**
     * Gets the {@link SessionFactory} being used to create the {@link GenericDao}s.
     * 
     * @return The {@link SessionFactory} being used to create the {@link GenericDao}s.
     */
    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    /**
     * Sets the {@link SessionFactory} to be used by the {@link GenericDao}s to obtain {@link Session} objects.
     * 
     * @param sessionFactory
     *            The {@link SessionFactory} to set.
     */
    public void setSessionFactory(final SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    private void registerDefaultScanners() {
        LocalFileSystemDirectoryScanner fileScanner = new LocalFileSystemDirectoryScanner();
        LocalFileSystemJarScanner jarScanner = new LocalFileSystemJarScanner();

        registerScanner(fileScanner);
        registerScanner(jarScanner);
    }

    private void registerDefaultGenericDaoInterceptor(final BeanDefinitionRegistry beanRegistry) {
        if (!beanRegistry.containsBeanDefinition(DEFAULT_HIBERNATE_GENERIC_DAO_ADVISOR_BEAN_NAME)) {
            ConstructorArgumentValues constructorValues = new ConstructorArgumentValues();
            constructorValues.addIndexedArgumentValue(0, new BaseReflectioner());
            constructorValues.addIndexedArgumentValue(1, new HibernateQueryExecutor(sessionFactory));

            RootBeanDefinition beanDefinition = new RootBeanDefinition(HibernateDaoAdvisor.class, constructorValues,
                    null);

            BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition,
                    DEFAULT_HIBERNATE_GENERIC_DAO_ADVISOR_BEAN_NAME);

            BeanDefinitionReaderUtils.registerBeanDefinition(holder, beanRegistry);
        }
    }

    private synchronized void createDefaultHibernateGenericDao() {
        if (defaultHibernateGenericDao == null) {
            defaultHibernateGenericDao = new HibernateGenericDao<Object>(sessionFactory);
        }
    }
}